iT邦幫忙

2023 iThome 鐵人賽

DAY 15
0
Modern Web

了不起的 Svelte系列 第 15

第 15 天: Svelte 的事件:事件修飾(二)

  • 分享至 

  • xImage
  •  

第 15 天: Svelte 的事件:事件修飾(二)

最後他起身,用一種不很確定的聲音,告訴我他要回去了。
「怎麼回事?」
「沒有事件會傳過來的。所有的事件都被 stopPropagation 給擋住了!」

~節錄自《The Great Svelte:第五章》

第 15 天要講的是

  1. 實作 Modal
  2. 使用事件修飾 self
  3. 使用事件修飾 stopPropagation

  昨天的內容中,我們介紹了在 Svelte 專案裡使用事件修飾的語法,並用 HTML 內建的元素 <dialog> 做出了互動視窗 (Modal) 的雛型。今天就讓我們把互動視窗的完整功能實做出來。

實作 Modal

  首先來到我們的 Modal.svelte,將預設開啟的程式碼修掉。接著說明一下該怎麼設計互動視窗 (Modal) 的開啟和關閉邏輯。我的想法是,既然互動視窗 (Modal.svelte) 是掛載在主要元件 (App.svelte) 下的其中一個子元件 (child component),又會跟著主要元件的狀態改變而開啟,那我們就把控制互動視窗 (Modal) 開啟與否的狀態當作一個變數,同樣也儲存到主要元件 (App.svelte) 當中吧。
  先來重新寫過我們的 Modal.svelte

/src/lib/Modal.svelte
<script>
  import { createEventDispatcher } from "svelte";
  export let showModal;

  let dialog;
  const dispatch = createEventDispatcher();
  const handleClose = () => dispatch('closeModal');
  
  setTimeout(() => {
    dialog = document.querySelector('dialog');
    // 刪除預設開啟的程式碼 dialog.showModal();
  }, 1);

  $: if (dialog && showModal) dialog.showModal();
  $: if (dialog && !showModal) dialog.close();
</script>

<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-noninteractive-element-interactions -->
<dialog on:close={handleClose}>
  <!-- svelte-ignore a11y-no-static-element-interactions -->
  <div>
    <div class="title">
      <h2>Forbidden</h2>
      <small><em>adjective</em> for·bid·den \fər-ˈbi-dᵊn \</small>
    </div>

    <hr />
    <div class="content">
      <p>
        You have no privilidge to enter domain beyond 87. Please register
        as member so we can sent you some spam email.
      </p>
    </div>
    <hr />
    <!-- svelte-ignore a11y-autofocus -->
    <div class="footer">
      <button autofocus on:click={handleClose}>close modal</button>
    </div>
  </div>
</dialog>
  • 第二行:import { createEventDispatcher } from "svelte";
      引入 createEventDispatcher 為客製化事件做準備。

  • 第三行:export let showModal;
      如同先前提到的,我們需要用一個主要元件 (App.svelte)當中的變數來表達互動視窗 (Modal) 開啟與否。同時我們也需要在代表互動視窗的子元件 (Modal.svelte) 準備一個變數,接收來自主要元件的狀態。而這個重要的角色就用showModal 這個變數來擔任。

  • 第六行:const dispatch = createEventDispatcher();
      初始化一個事件產生器。

  • 第七行:const handleClose = () => dispatch('closeModal');
      宣告一個函式。這個函式會作為事件處理器,向主要元件 (App.svelte) 傳遞 closeModal 這個客製化事件。

  • 第十一行:// 刪除預設開啟的程式碼 dialog.showModal();
      我們再昨天的程式碼當中寫下這一行,讓互動式窗 (Modal) 預設開啟。今天我們要讓互動視窗 (Modal) 有條件地開啟,所以記得把這一行刪掉。

  • 第十四行:$: if (dialog && showModal) dialog.showModal();
      互動視窗 (Modal) 開啟的條件是:當 Javascript 的變數 dialog 開始代表 HTML 元素 <dialog>,並且 showModaltrue 時。利用呼叫 showModal() 這個內建的函式來開啟互動式窗 (Modal)。

  • 第十五行:$: if (dialog && !showModal) dialog.close();
      互動視窗 (Modal) 關閉的條件是:當 Javascript 的變數 dialog 開始代表 HTML 元素 <dialog>,並且 showModalfalse 時。利用呼叫 close() 這個內建的函式來關閉互動式窗 (Modal)。

  • 第十九行:<dialog on:close={handleClose}>
      當 dialog 因為使用者按下【Esc】鍵而被關掉時,記得向主要元件 (App.svelte) 傳遞 closeModal 事件。

  • 第三十七行:<button autofocus on:click={handleClose}>close modal</button>
      當這個 <button> 被按下時,也記得向主要元件 (App.svelte) 傳遞 closeModal 事件。

  接著來到主要元件 App.svelte,我們要設定何時應該開啟互動視窗 (Modal),以及接收來自互動視窗的 closeModal 事件,以便即時更新狀態。

/src/App.svelte
<!-- 在 Javascript 當中 import Counter -->
<script>
  import Counter from './lib/Counter.svelte';
  import Modal from './lib/Modal.svelte';

  let count = 87;
  let showModal = false;

  let someState = 'TheGreatSvelte';
  const sparkle = (text) => {
    const sparkles = ['★', '☆', '✧', '✪'];
    const randomSparkles = () => sparkles[Math.floor(Math.random() * sparkles.length)];
    const sparkledText = text.split('').reduce((a, c) => a + randomSparkles() + c, '');
    return sparkledText;
  }

  const href = 'https://ithelp.ithome.com.tw/users/20120178/ironman/7031';

  const handleClick = (e) => {
    console.log(e);
    const tobeCount = count + e.detail;
    if (!(tobeCount > 87)) count = tobeCount;
    else showModal = true;
  }
</script>

<main>
  <!-- 在 HTML 當中直接嵌入 Counter -->
  <Counter {count} on:changeCount={handleClick}/>

  <p class='comment'>Check out <a {href}>Svelte Tutorial</a>, the awesome article powered by {sparkle(someState)}!</p>
</main>

<Modal {showModal} on:closeModal={() => showModal = false}/>
  • 第四行:import Modal from './lib/Modal.svelte';
      引入我們需要的 Svelte 元件,也就是 Modal.svelte

  • 第六行:let count = 87;
      宣告變數 count,並將初始值設定在 87

  • 第七行:let showModal = false;
      宣告變數 showModal,並將初始值設定為 false。這個變數就是在主要元件 (App.svelte) 當中,要用來控制互動視窗 (Modal) 開啟與否的變數。

  • 第十九行:const handleClick = (e) => {
      如同在第 13 天當中實作的,利用 handleClick 這個事件處理器去處理來自 <Counter> 的事件。

  • 第二十一行:const tobeCount = count + e.detail;
      只不過今天我們要給 count 做一個限制。先在處理器內宣告一個變數 tobeCount,利用這個變數預判 count 將要發生的變化。

  • 第二十二行:if (!(tobeCount > 87)) count = tobeCount;
      如果 count 將要變成一個不超過 87 的數字,那麼就讓這個變化發生吧。

  • 第二十三行:else showModal = true;
      否則的話,不僅不讓這個變化發生,還要跳出互動視窗 (Modal) 來警告使用者。

  • 第二十九行:<Counter {count} on:changeCount={handleClick}/>
      如同在第 13 天當中實作的,把 count 作為 Property 傳入 <Counter>,並加上事件處理器 handleClick 處理 changeCount 客製化事件。

  • 第三十四行:<Modal {showModal} on:closeModal={() => showModal = false}/>
      加入 <Modal>,並將 showModal 作為 Property 傳入 <Modal>,並加上行內 (inline) 事件處理器 () => showModal = false,當街收到來自 <Modal>closeModal 事件,就將 showModal 這個變數重新賦值為 false

https://i.ibb.co/BtgBBmt/15.gif
圖一、count 只能到 87,不能再高了

  根據以上程式碼,我們能夠做出一個不允許 count 超過 87,並且在 count 即將超過 87 時,開啟互動視窗 (Modal) 提醒使用者的奇妙專案了。已經開啟的互動視窗 (Modal) 則可以藉由按下鍵盤【Esc】鍵,或是點擊互動視窗 (Modal) 當中的 <button> 來關閉。
  但我們平常使用的互動視窗 (Modal),通常還有另一種關閉的作法,那就是點擊互動視窗 (Modal) 外的空白處,如圖二。

https://ithelp.ithome.com.tw/upload/images/20230930/201201785jzQrIXPnP.png
圖二、常見關閉互動視窗的手段

  要怎麼做到這件事呢?其實只要讓 <dialog> 能夠接收點擊事件,並且同樣加上事件處理器 handleClose 去關閉互動視窗 (Modal) 就好。也就是讓 <dialog> 變成這樣:

<dialog on:close={handleClose} on:click={handleClose}>

  為什麼這樣能行呢?因為互動視窗 (Modal) 雖然看起來只有中間那個對話框,但實際上是藉由 CSS 的 margin 占滿整個畫面的,如圖三。所以雖然說是點擊互動視窗外的空白處,但實際上還是點在 <dialog> 這個元素上面。

https://ithelp.ithome.com.tw/upload/images/20230930/20120178MlBf0RUNLe.png
圖三、看看 <dialog> 廣大的 margin

  但這樣做其實會有一個明顯的問題,那就是點擊互動視窗的對話框本身也會觸發點擊 (click) 事件,進而呼叫事件處理器 handleClose,導致互動視窗也被關閉起來了。這就不是我們希望看到的了。該怎麼避免呢?

使用事件修飾 self

  很簡單,還記得我們昨天討論過的事件修飾,其中一個修飾詞 self,限制事件必須發生在指定的 HTML 元素本身,符合條件才會呼叫事件處理器。所以只要在 on:click 加上我們的事件修飾 self

<dialog on:close={handleClose} on:click|self={handleClose}>

  並且記得用 CSS 把 <dialog> 內的 HTML 元素做成如圖四,也就是 <dialog> 元素本身沒有 padding,而由 <dialog> 內 HTML 元素的 margin 來做出想要的空間感。如此一來,點擊互動視窗的對話框,其 event.target 只能是 <dialog> 內的 HTML 元素,而不會是 <dialog> 本人,就不符合 on:click|self 的條件,就不會呼叫 handleClose 了。

https://ithelp.ithome.com.tw/upload/images/20230930/20120178Lz2r2ByquX.png
圖四、用 <dialog> 內的 HTML 元素占滿整個對話框

使用事件修飾 stopPropagation

  用事件修飾詞 self 可以完美達成我們的需求。用 stopPropagation 也同樣可以。既然我們昨天學了這麼多事件修飾詞,就來嘗試看看不同修飾詞的功能吧。stopPropagation 的用法是,阻止發生在 HTML 元素的事件繼續往更高一層的 HTML 元素傳遞 (bubble)。所以用 stopPropagation 的邏輯,是藉由在 <dialog> 內的 HTML 元素放置一個 on:click|stopPropagation,來把所有點擊在互動視窗對話框的事件都攔下來:

<dialog on:close={handleClose} on:click={handleClose}>
  <!-- svelte-ignore a11y-no-static-element-interactions -->
  <div on:click|stopPropagation>
    <!-- 在這邊省略顯示內部的 HTML 元素 -->
  </div >
</dialog>

https://i.ibb.co/CmbDsXB/15.gif
圖五、一個完美的 87 分守門員

  好的~我們今天終於把互動視窗給完成了,並且也實際練習了兩個事件修飾詞的使用。今天的內容就到這邊了,關於最後實作完成的互動視窗程式碼可以看文末的附錄段落,或是到 Github 的資源庫看完整程式碼。謝謝各位讀者。

附錄:用 self 實作互動視窗

/src/lib/Modal.svelte
<script>
  import { createEventDispatcher } from "svelte";
  export let showModal;

  let dialog;
  const dispatch = createEventDispatcher();
  const handleClose = () => dispatch('closeModal');
  
  setTimeout(() => {
    dialog = document.querySelector('dialog');
    // 刪除預設開啟的程式碼 dialog.showModal();
  }, 1);

  $: if (dialog && showModal) dialog.showModal();
  $: if (dialog && !showModal) dialog.close();
</script>

<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-noninteractive-element-interactions -->
<dialog on:close={handleClose} on:click|self={handleClose}>
  <!-- svelte-ignore a11y-no-static-element-interactions -->
  <div>
    <div class="title">
      <h2>Forbidden</h2>
      <small><em>adjective</em> for·bid·den \fər-ˈbi-dᵊn \</small>
    </div>

    <hr />
    <div class="content">
      <p>
        You have no privilidge to enter domain beyond 87. Please register
        as member so we can sent you some spam email.
      </p>
    </div>
    <hr />
    <!-- svelte-ignore a11y-autofocus -->
    <div class="footer">
      <button autofocus on:click={handleClose}>close modal</button>
    </div>
  </div>
</dialog>

<style>
  dialog {
    color: var(--color-text);
    max-width: 20em;
    border-radius: 0.2em;
    border: none;
    margin: auto;
    box-shadow: 0.5em 0.5em 1.5em rgba(0, 0, 0, 0.1),
      -0.5em -0.5em 1.5em rgba(0, 0, 0, 0.1);
  }
  dialog::backdrop {
    background: rgba(0, 0, 0, 0.4);
  }

  dialog > div {
    padding: 2em;
  }

  dialog > div > div {
    margin: 1em 0;
  }

  button {
    font-size: 1.1em;
    padding: 0.5em;
    border-radius: 0.2em;
    border: none;
    outline: none;
    cursor: pointer;
  }

  button:hover {
  filter: brightness(1.02);
  }
</style>

附錄:用 stopPropagation 實作互動視窗

/src/lib/Modal.svelte
<script>
  import { createEventDispatcher } from "svelte";
  export let showModal;

  let dialog;
  const dispatch = createEventDispatcher();
  const handleClose = () => dispatch('closeModal');
  
  setTimeout(() => {
    dialog = document.querySelector('dialog');
    // 刪除預設開啟的程式碼 dialog.showModal();
  }, 1);

  $: if (dialog && showModal) dialog.showModal();
  $: if (dialog && !showModal) dialog.close();
</script>

<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-noninteractive-element-interactions -->
<dialog on:close={handleClose} on:click={handleClose}>
  <!-- svelte-ignore a11y-no-static-element-interactions -->
  <div on:click|stopPropagation>
    <div class="title">
      <h2>Forbidden</h2>
      <small><em>adjective</em> for·bid·den \fər-ˈbi-dᵊn \</small>
    </div>

    <hr />
    <div class="content">
      <p>
        You have no privilidge to enter domain beyond 87. Please register
        as member so we can sent you some spam email.
      </p>
    </div>
    <hr />
    <!-- svelte-ignore a11y-autofocus -->
    <div class="footer">
      <button autofocus on:click={handleClose}>close modal</button>
    </div>
  </div>
</dialog>

<style>
  dialog {
    color: var(--color-text);
    max-width: 20em;
    border-radius: 0.2em;
    border: none;
    margin: auto;
    box-shadow: 0.5em 0.5em 1.5em rgba(0, 0, 0, 0.1),
      -0.5em -0.5em 1.5em rgba(0, 0, 0, 0.1);
  }
  dialog::backdrop {
    background: rgba(0, 0, 0, 0.4);
  }

  dialog > div {
    padding: 2em;
  }

  dialog > div > div {
    margin: 1em 0;
  }

  button {
    font-size: 1.1em;
    padding: 0.5em;
    border-radius: 0.2em;
    border: none;
    outline: none;
    cursor: pointer;
  }

  button:hover {
  filter: brightness(1.02);
  }
</style>

上一篇
第 14 天: Svelte 的事件:事件修飾(一)
下一篇
第 16 天:Svelte 中的邏輯運作:`each` 邏輯區塊(一)
系列文
了不起的 Svelte30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言